读书笔记-Effective JavaScript

让自己习惯 JavaScript

  • 了解你使用的JavaScript版本

    • Web浏览器,并不支持让程序员指定JS的版本来执行代码

    • 严格模式(”use strict”),允许你选择在受限制的JS版本中禁用一些问题较多或者出错的特性

    • 不要试图将严格模式和非严格模式的文件连接在一起,建议在严格模式下编写代码(ES5 中增加的,兼容ES3)

    • 将其自身包裹在立即调用的函数(IIFE)表达式中的方式连接多个文件,是比较安全的

  • 理解JavaScript的浮点数

    • 大多数编程语言都有几种数值型数据类型,但JS只有一种,就是 number,也就是常理解的 double(双精度浮点数)

    • JS中所有 位运算符 的工作方式是:将操作数转换为整数,然后使用整数位模式进行运算,最后将结果转换为标准的JS浮点数(3步),即将数字视为32位的有符号整数

    • 浮点数的操作是出了名的不准确

      0.1 + 0.2 // 0.300000000000004
      0.1 + 0.2 + 0.3 ≠ 0.1 +(0.2 + 0.3)
      
    • 当心浮点运算中的精度陷阱,一般都是转化为最小单位(尽可能转换成整数)去计算,因为整数在表示时不需要舍入

  • 当心隐式的强制转换

    • 类型错误可能被隐式的强制转换所隐藏

    • 重载运算符 + 是进行加法运算还是字符串连接操作取决于其参数类型

    • 对象通过valueOf方法强制转换为数字,通过toString方法强制转换为字符串

    • 具有valueOf方法的对象应该实现toString方法,返回一个valueOf方法产生的数字字符串表示

    • JS中有且只有7种假值:false、0、-0、””、NaN、null 和 undefined

    • 测试一个值是否为未定义的值,应该使用 typeOf或者与undefined进行比较而不是真值运算

  • 原始类型优于封装对象

    • 5个原始类型:布尔值、数字、字符串、null 和 undefined

    • 隐式封装:当对原始值提取属性和进行方法调用时,他表现得就像已经使用了对应的对象类型封装了该值一样。例如:String的原型对象有一个toUpperCase的方法,你可以对这个原始字符串值调用这个方法

    • 当做相等比较时,原始类型的封装对象 ≠ 原始类型值

    • 获取和设置原始类型值的属性会隐式地创建封装对象,字符串对象的引用在用完之后立即被销毁,所以不能给字符串添加属性

      "hello".property = 1;
      "hello".property; // undefined
      
  • 避免对混合类型使用 == 运算符

    • 当参数类型不同时,==运算符应用了一套难以理解的隐式强制转换规则

    • 当使用 === 运算符时,不需要涉及任何的隐式强制转换就明白你的比较运算符

    • 当比较不同类型的值时,使用你自己的显示强制转换使程序的行为更清晰

      null == undefined // true
      string/number/boolean == Date // Date.toString() --> Date.valueOf()
      string/number/boolean == 非 Date // 非 Date.valueOf() --> 非 Date.toString()
      
  • 了解分号插入的局限:分号插入原则

    • 仅在 } 标记之前、一个或多个换行之后和程序输入的结尾

    • 仅在紧接着的标记不能被解析的时候推导分号

    • 在以(、[、+、- 或 / 字符开头的语句钱决不能省略分号

    • 当脚本连接的时候,在脚本之间显式插入分号

    • 在 return、throw、break、contine、++ 或 – 的参数之前决不能换行

    • 分号不能作为for循环的头部或空语句的分隔符

  • 视字符串为16位的代码单元序列

    • JS字符串有16位的代码单元组成,而不是由Unicode代码点组成

变量作用域

  • 尽量少用全局对象

    • 多使用局部变量

    • 避免对全局对象添加属性

    • 使用全局对象来做平台特性检测

  • 始终声明局部变量

    • 使用 var(函数作用域) 和 let(for循环局部作用域) 来声明新的局部变量

    • 考虑使用lint工具帮助检查未绑定的变量

  • 不要使用with

  • 熟练掌握闭包

    • JavaScript允许你引用在当前函数以外定义的变量(内部函数可以访问外部成员变量,引了,该函数就成为闭包)

    • 闭包比创建它们的函数有更长的生命周期

    • 闭包在内部存储其外部变量的而引用,并能读写这些变量

      function sandwichMaker(){
          var magicIngredient = "peanut butter";
          function make(filling){
              return magicIngredient + " and " + filling
          }
          return make;
      }
      var f = sandwichMaker();
      f('jelly') // peanut butter and jelly
      f('bananas') // peanut butter and bananas
      
      //那些在其所涵盖的作用域内跟踪变量的函数,称为闭包,make函数就是一个闭包
      
  • 理解变量生命提升

    • JavaScript隐式地提升(hoists)声明部分到封闭函数的顶部,而将赋值留在原地,换句话说,变量的作用域是整个函数,但仅在var语句出现的位置进行赋值

    • 重声明变量被视为单个变量

    • 请手动提升局部变量的声明,从而避免混淆

  • 使用立即调用的函数表达式(IIFE)创建局部作用域

    • 闭包存储的是其外部变量的引用,而不是值

      function wrapElements(a){
          var result = [], i, n;
          for(i = 0, n = a.length ; i < n ; i++){
              result[i] = function(){ return a[i]; };
          }
          return result;
      }
      
      由于function中引用了变量i,所以该函数为每个result[i]都创建了一个闭包,且保存着对i的引用
      所以i = a.length;即result[1...a.length-1]都等于a[a.length],即 undefined
      
    • 立即调用的函数表达式

      function wrapElements(a){
          var result = [];
          for(var i = 0, n = a.length ; i < n ; i++){
              (function(j){
                  result[i] = function(){ return a[i]; };
              })(i);
          }
          return result;
      }
      
  • 当心命名函数表达式笨拙的作用域

    • var f = function(){}
  • 当心局部块函数声明笨拙的作用域

    • 始终将函数声明至于程序或被包含的函数的最外层,以避免不可移植的行为

    • 局部块函数声明: 使用var声明和有条件的赋值语句代替有条件的函数声明

  • 避免使用eval创建局部变量

    • 不要赋予外部调用者能改变函数内部作用域的能力,即 函数内部过分依赖于 动态绑定(eval(“var x = 1”))

    • 如果 eval 函数代码可能创建全局变量,将此调用封装到嵌套的函数中以防止作用域污染

      var y = "global";
      function test(src){
          (function(){ eval(src); })();
          return y;
      }
      
      test("var y = 'local';") // global
      test("var z = 'local';") // global
      
  • 间接调用eval函数优于直接调用(Node里面没有臭名昭著的eval)

    • 尽可能间接调用eval函数,而不要直接调用eval函数:(0,eval)(src)

使用函数

  • 理解函数调用、方法调用及构造函数调用之间的不同

    • 在方法调用中是由调用表达式自身来确定this变量的绑定。绑定到this变量的对象被称为调用接收者(receiver)

    • 方法调用将被查找方法属性的对象作为调用接收者

    • 函数调用将全局对象作为其接受者,一般很少使用函数调用语法来调用方法

    • 构造函数需要通过new运算符调用,并产生一个新的对象作为其接收者

  • 熟练掌握高阶函数

    • 高阶函数就是那些将函数作为 参数 或者 返回值 的函数

    • 当发现自己在重复地写一些相同的模式时,学会借助于一个高阶函数(提炼逻辑,让callback作为参数传递)可以使代码更简洁、更高效、更可读,学会发现可以被高阶函数所取代的常见的编码模式

    • 掌握现有库中的高阶函数,比如数组的map

  • 使用call方法自定义接受者来调用方法

    • f.call(obj,arg1,arg2,arg3)与 f(arg1,arg2,arg3)不同的是第一个参数提供了一个显式的接收者对象

    • 使用 call 方法自定义接收者来调用函数

    • 使用 call 方法可以调用在给定的对象中不存在的方法

    • 使用 call 方法定义高阶函数允许使用者给回调函数指定接收者

  • 使用apply方法通过不同数量的参数调用函数

    • 使用 apply 方法制定一个可计算的参数数组来调用可变参数的函数,apply方法需要一个参数数组,然后将数组的每一个元素作为调用的单独参数调用该函数

    • 使用 apply 方法的第一个参数给可变参数的方法提供一个接收者

  • 使用arguments创建可变参数的函数

    • JavaScript给每个函数都隐式得提供了一个名为arguments的局部变量。arguments对象给实参提供了一个类似的数组接口,它为每个实参提供了一个索引属性,还包含一个length属性用来指示参数的个数,从而可以通过遍历arguments对象的每个元素来实现可变元数的函数

    • 如果提供了一个便利的可变参数的函数,也最好提供一个需要显示指定数组的固定元数版本

      function average(){
          return averageOfArray(arguments);
      }
      
  • 永远不要修改 arguments 对象

    • 使用 [].slice.call(arguments) 将 arguments 对象复制到一个真正的数组中再进行修改
  • 使用变量保存 arguments 的引用

    • 先用变量绑定到 arguments,明确作用域之后,再在嵌套函数中引用它
  • 使用 bind 方法提取具有确定接受者的方法

    • 提取一个方法不会将方法的接收者绑定到该方法的对象上

    • 当给高阶函数传递对象方法时,使用匿名函数在适当的接收者上调用该方法

    • 使用 bind 方法创建绑定到适当接收者的函数

  • 使用 bind 方法实现函数柯里化(不懂)

    • 将函数与其参数的一个子集绑定的技术成为函数柯里化

    • function.bind(null, arg1, arg2)

  • 使用闭包而不是字符串来封装代码

    • 当将字符串传递给eval函数以执行时,不要在字符串中包含局部变量的引用,容易在函数中引起冲突

    • 优先接收函数,而不是eval执行的字符串,即

      function repeat(n, action){
          for(var i = 0 ; i < n ; i++){
              action();
          }
      }
      
  • 不要信赖函数的toString方法

    • 在不同的引擎下调用toString方法的结果可能不同,所以绝不要信赖函数源代码的详细细节

    • toString方法的执行结果并不会暴露存储在闭包中的局部变量值

    • 通常情况下,应该避免使用函数对象的toString方法

  • 避免使用非标准的栈检查属性

    • 调用栈实质当前正在执行的活动函数链

    • 不要使用非标准的 arguments.caller 和 arguments.callee 属性

对象和原型

  • 理解 prototype、getPrototypeOf 和 proto 之间的不同(有一个重点图)

    • C.prototype 属性是 new C() 创建的对象的原型

    • Object.getPrototypeOf(obj)是ES5中检索对象原型的标准函数

      Object.getPrototypeOf(u) === User.prototype
      
    • obj.proto 是检索对象原型的非标准方法(不支持ES5的情况下采用)

    • 类:是由一个构造函数 和 一个关联的原型 组成的一种设计模式

      function User(name, passwordHash){
          this.name = name;
          this.passwordHash = passwordHash;
      }
      
      User.prototype.toString = function(){
          return "sdsds";
      }
      
      var instance = new User("abc","abc");
      
      Function.prototype(.call/.bind/.apply) --> User(.prototype) --> User.prototype(.toString) --> instance(.name/.passwordHash)
      
  • 使用 Object.getPrototypeOf 函数而不要使用 proto 属性

    • 获取对象原型的标准API是 Object.getPrototypeOf()

    • 在支持proto属性的非ES5环境中实现 getPrototypeOf

      if(type Object.getPrototypeOf === "undefined"){
          Object.getPrototypeOf = function(obj){
              var t = typeof obj;
              if(!t || ( t !== "onject" && t !== "function")){
                  throw new TypeError("not an object");
              }
              return obj._proto_;
          }
      }
      
  • 始终不要修改 proto 属性

    • 使用Object.create函数给新对象设置自定义的原型
  • 使构造函数与 new 操作符无关

    • 防范误用构造函数可能不值得费心去做,但在跨大型代码库中共享构造函数或者构造函数来自一个共享库的时候,就需要多加防范了

    • 当一个函数期望使用new操作符调用时,清晰地文档化该函数

    • 通过使用 new 操作符或Object.create 方法在构造函数定义中调用自身使得该构造函数与调用语法无关

  • 在原型中存储方法

    • 原型是共享方法的渠道之一,将方法存储于原型中由于存储在示例对象中

    • 将方法存储在示例对象中将创建该函数的多个副本,因为每个实例对象都有一份副本

  • 使用闭包存储私有数据

    • 闭包变量是私有的,只能通过局部的引用获取

    • 将局部变量作为私有数据从而通过方法实现信息隐藏

  • 只将实例状态存储在实例对象中

    • 共享可变数据可能会出现问题,因为原型是被其所有的实例共享的

    • 将可变的实力状态存储在实例对象中

  • 认识到 this 变量的隐式绑定问题

    • this 变量的作用域总是由其最近的封闭函数所确定的

    • 使用一个局部变量(通常命名为 self、me 和 that)使得this绑定对于内部函数是可用的

数组和字典

  • 原型污染

    • 定义:Object 是 js 对象的根,如果在 Object.prototype 上增加任何方法,那么随之使用 for…in 遍历对象属性的时候,此时增加的方法也会计算进去从而造成的原型污染,此外,一些特殊命名的变量名也会对对象造成污染。

    • 原因:for…in 循环除了枚举出对象”自身”的属性外,还会枚举出继承过来的属性

    • 预防:

      • 使用 Object.create(null) 来创建一个没有原型的对象,因此原型污染就无法影响这样的对象行为

      • 使用 hasOwnProperty 方法(只过来对象自身属性)以避免原型污染: var hasOwn= Object.prototype.hasOwnProperty; hasOwn.call(dic,”alice”)

      • 别在 Object.prototype 中增加属性,若不小心增加了属性,也要用 Object.defineProperty方法把属性 enumerable置成false

  • 循环和迭代

    • 使用数组而不要使用字典来存储有序集合

    • for…in 循环会挑选一定的顺序来枚举对象的属性,如果是有序的,还是用for循环遍历

    • 避免在 for…in 期间修改对象,如果需要修改,应该使用while或for循环

    • 数组的循环优先使用for循环而不是for…in循环

    • 使用迭代方法由于循环:forEach,map,every,some

    • 处理数组内容的改变:map,筛选数组的内容:filter

    • 循环只有一点优于迭代函数,那就是控制流操作,如 break 和 continue

  • 创建 数组、对象,直接使用 var x = [],{} 即可,因为使用 Array 和 Object 不能保证是不是被污染了

库和API设计

-【参数】 保持一致的约定

- 约定参数的顺序

    - 宽度第一,然后高度: new Widget(320, 640) // width:320, height:240

    - CSS顺时针约定:top,right,bottom,left

-【参数】 在变量命名和函数签名中使用一致的约定

-【多元化】 不要偏离用户在其他开发平台中很可能遇到的约定

-【检查】真值测试是实现参数默认值的一种简明的方式(this.hostname = hostname || “localhost”),但不适用于允许0、NaN或空字符串为有效参数的情况,比如this.hostname就是等于undefined

-【检查】应该提供参数默认值应当采用测试undefined的方式,而不是检查arguments.length,即 this.width = width === undefined ? 320 : width

-【参数】使用接收关键字参数的选项对象,options object,既接收Alert({xxx:yyy})的方式,避免参数过多,API失去可扩展性

-【设计】避免不必要的状态,API有时候被归为两类:有状态的(Date对象)和无状态的(幂等性,输入对应固定输出),无状态的API比有状态的API,从易用性,学习容易性而言,更优。无状态的API简洁,易于扩展

-【设计】API绝对不应该重载与其他类型有重叠的类型,一个方法不应该既接收type 1又接收type 2,当重载一个结构类型与其他类型时,先测试其他类型

- 判断是不是数组:Array.isArray 和 toString.call(x) === "[Object Array]"
- 将对象转换成数组:[].slice.call(arguments)

-【设计】避免过度的强制转换,应该显示的转换,用代码写出来,而不是交给编译器

-【设计】支持方法链

- 使用方法连来连接无状态的操作
- 通过在无状态的方法中返回新对象来支持方法链
- 通过在有状态的方法中返回this来支持方法连

并发

  • 异步的强大能力:JS的运行到完成机制(run-to-complete),在系统里维护了一个按时间发生顺序的内部事件队列,一次调用一个已注册的回调函数,通过轮询这个队列来完成异步的操作,有序的调用回调函数。

  • Workers API提供了线程的能力,在一个完全隔离的状态下执行,没有获取全局作用域或应用程序主线程Web页面内容的能力

  • 应该避免将可被并行执行的操作顺序化,嵌套的回调函数应该用Promise

  • 对于异步的代码,多步的处理通常被分隔到时间队列的单独伦次中,因此不可能将它们全部包装在一个try语句块中。事实上,异步的API甚至根本不可能抛出异常,因为,当一个异步的错误发生时,没有一个明显的执行上下文来抛出异常!因此,异步API更加倾向于将错误表示为回调函数的特定参数,即err,在Nodejs平台中,err是回调函数的第一个参数

  • 目前典型的JS环境中一个递归函数同步调用自身过多次会导致失败,原因是:存储过多的栈帧需要的空间量会消耗JS环境分配的空间

    • 循环不能是异步的

    • 使用递归函数在事件循环的单独轮次中执行迭代

      function downloadOneAsync(urls, onsuccess, onfailure){
          var n = urls.length;
          function tryNextURL(i){
              if(i>=n){
                  onfailure("xxxxxxx");
                  return;    
              }
              downloadAsync(url[i], onsuccess,function(){
                  tryNextURL(i+1);
              });
          }
          tryNextURL(0);
      } 
      
  • 避免在主事件队列中执行代价高昂的算法,在支持Worker API的平台,该API可以用来在一个独立的事件队列中运行场计算程序,在Work API不可用或代价昂贵的环境中,考虑将计算程序分解到事件循环的多个轮次中

  • 使用计数器来执行并行操作可以避免竞争,Js应用程序中的事件发生时不确定的,即顺序是不可预测的

  • 同步地调用异步的回调函数扰乱了预期的操作序列,并可能导致意想不到的交错代码,也可能导致栈溢出或者错误的异常处理

Promise
  • promise代表最终值,即并行操作完成时最终产生的结果

  • 使用promise组合不同的并行操作,避免数据竞争

  • 在要求有意的竞争条件时,使用select

如需转载,请注明出处